<?php
/**
 * Created by PhpStorm.
 * User: DELL-PC
 * Date: 2018/5/19
 * Time: 13:06
 */

namespace Aoe\Intent;


use Aoe\Attributes\Container\Argument;
use Aoe\Intent\Request\Request;
use function strtr;

/**
 * # 可配置化的路由服务
 *      规则:   /:module/:controller/:action 默认是正则解析，可用过intent自定义解析
 *      实体:  :module 默认是正则解析，可以通过callback自定义解析办法
 *
 *   - 规则和实体都会被解析并缓存
 */
class Router
{
    /**
     * 索引-路由规则
     */
    const string RULES      = 'rules';
    /**
     * 索引-路由实体
     */
    const string ENTITIES   = 'entities';
    /**
     * 索引-自定义路由实体解析回调
     */
    const string CALLBACKS  = 'callbacks';
    /**
     * 索引-解析结果-正则表达式
     */
    const string REGEX      = 'regex';
    /**
     * 索引-解析结果-文件扩展符
     */
    const string EXT        = 'ext';
    /**
     * 索引-解析结果-Params
     */
    const string PAIRS     = 'pairs';
    /**
     * 配置-列表-路由规则
     * @var array<array{
     *     module?: string,
     *     controller?: string,
     *     action?: string,
     *     method?: string,
     *     ext?: string,
     * }>
     */
    private array $rules = [];
    /**
     * 配置-列表-实体解析结果
     *
     *  * 经过解析，不同于原配置
     *
     * @var string[]
     */
    private array $entities    = [];
    /**
     *  配置-自定义路由实体解析方法
     * @var Array<callable(string $value, array $param, string $key): void>
     */
    private array $callbacks   = [];
    /**
     * 内部使用-列表-完成正则化的路由规则
     *
     * @var string[]
     */
    private array $parsed_rules  = [];
    /**
     * 内部使用-列表-完成解析的路由实体
     *
     * @var string[]
     */
    private array $rules_require = [];
    
    /**
     * @param array{
     *     rules: string[],
     *     intents?: array<array | callable(Request $request): void>,
     *     entities: string[],
     *     callables: string[],
     * } $config
     */
    public function __construct(#[Argument] array $config)
    {
        // 解析路由实体
        isset($config[self::ENTITIES]) and $this->_prepare_entities($config[self::ENTITIES]);
        
        // 路由规则懒解析，自定义内容直接赋值
        foreach ([self::RULES, self::CALLBACKS,] as $property)
            $this->_set_($property, $config);
    }
    
    /**
     * ## 对请求进行路由计算
     *
     * @param Request $request
     *
     * @return Section | false
     */
    public function dispatch(Request $request): false | Section
    {
        $command = trim($request->getPathinfo(), '/');
        // 自动获取后缀
        $ext = pathinfo($command, PATHINFO_EXTENSION);
        
        foreach ($this->rules as $regex => $param) {
            if (!$request->fit($param)) continue;
            
            $values = $this->_match_regex($command, $regex, $param);
            if ($values === false) continue;
            
            // 记录路由规则和文件后缀
            $values[self::REGEX]      = $regex;
            $values[self::EXT]        = $ext;
            if (isset($values[self::PAIRS])) $values[self::PAIRS] = new Params($values[self::PAIRS]);
            return new Section($values);
        }
        
        return false;
    }
    
    /**
     * ## 反查有效的 Command
     *
     * @param array   $values 参数
     * @param ?string $regex  路由规则
     *
     * @return false|string  url
     * @noinspection PhpUnused
     */
    public function feedback(array $values, ?string $regex = null): false | string
    {
        $regex = $regex ?? $values[self::REGEX] ?? null;
        // regex 是Route关键字，为防止误操作，保证程序流畅，禁止输出该变量
        unset($values[self::REGEX]);
        
        // 处理后缀
        $ext = (
            isset($values[self::EXT])
            and isset($this->entities[':' . self::EXT])
                and $this->_match_entity(self::EXT, $values[self::EXT])
        ) ? $values[self::EXT] : '';
        unset($values[self::EXT]);
        
        // 指定了路由规则，直接验证
        if ($regex !== null && isset($this->rules[$regex]))
            return $this->_create_command_by_regex($regex, $this->rules[$regex], $values, $ext);
        
        // 轮询路由表，匹配验证
        foreach ($this->rules as $regex => $param) {
            $url = $this->_create_command_by_regex($regex, $param, $values, $ext);
            if ($url === false) continue;
            return $url;
        }
        
        return false;
    }
    
    /**
     * 编译并初始化路由实体
     *
     * @param array $entities
     */
    private function _prepare_entities(array $entities): void
    {
        foreach ($entities as $name => $regex) {
            $this->entities[':' . $name] = "(?<$name>$regex)";
        }
    }
    
    private function _set_($property, $config): void
    {
        $this->$property = (isset($config[$property]) and is_array($config[$property])) ?
            $config[$property] : [];
    }
    
    /**
     * 路由匹配具体操作
     *
     * @param string $query
     * @param string $regex
     * @param array  $param
     *
     * @return boolean|array
     */
    private function _match_regex(string $query, string $regex, array $param = []): false | array
    {
        //转换为正则路由
        if (!preg_match("@^/.*/[isUx]*$@i", $regex)) {
            $regex = $this->_normalize_regex($regex);
        }
        
        //路由匹配
        if (preg_match($regex, $query, $match)) {
            return $this->_parse_match($match, $param);
        }
        
        return false;
    }
    
    /**
     * 正则化路由规则
     *
     * @param string $regex
     *
     * @return string
     */
    private function _normalize_regex(string $regex): string
    {
        if (!isset($this->parsed_rules[$regex])) {
            $this->parsed_rules[$regex] = "@^" . strtr($regex, $this->entities) . "@i";
        }
        return $this->parsed_rules[$regex];
    }
    
    /**
     * 返回路由匹配后获取的参数
     *
     * @param array $match
     * @param array $param
     *
     * @return array
     */
    private function _parse_match(array $match, array $param): array
    {
        foreach ($match as $key => $value) {
            if (is_numeric($key) and isset($param[$key - 1])) {
                $param[$param[$key - 1]] = $value;
                unset($param[$key - 1]);
            } elseif (is_string($key) and !isset($param[$key])) {
                $this->_prepare_value($key, $value, $param);
            }
        }
        return $param;
    }
    
    /**
     * 扩展接口（自定义实体处理接口）
     *
     * @param string $key
     * @param string $value
     * @param array  $param
     */
    private function _prepare_value(string $key, string $value, array &$param): void
    {
        if (isset($this->callbacks[$key]) and is_callable($this->callbacks[$key])) {
            $function    = $this->callbacks[$key];
            $param[$key] = $function($value);
        } else {
            $param[$key] = $value;
        }
    }
    
    /**
     * 路由实体匹配
     *
     * @param string $key
     * @param        $value
     *
     * @return bool|int
     */
    private function _match_entity(string $key, $value): bool | int
    {
        $entity = ':' . $key;
        
        if (!isset($this->entities[$entity])) return false;
        
        if (!isset($this->builders[$key]) or !is_callable($this->builders[$key]))
            return preg_match('@^' . $this->entities[$entity] . '$@i', $value);
        
        $function = $this->builders[$key];
        return $function($this->entities[$entity], $value);
    }
    
    private function _create_command_by_regex($regex, $param, $values, $ext): bool | string
    {
        $replace = [];
        
        foreach ($this->_get_regex_require($regex) as $key) {
            $realKey = substr($key, 1);
            if ( // 必需参数不匹配
                !isset($values[$realKey]) or
                !$this->_match_entity($realKey, $values[$realKey])
            ) {
                return false;
            }
            $replace[$key] = $values[$realKey];
            unset($values[$realKey]);
        }
        
        if (empty($param)) $param = [];
        
        foreach ($param as $k => $v) {
            // 参数不匹配
            if (isset($values[$k]) and $values[$k] !== $v
            ) {
                return false;
            }
            unset($values[$k]);
        }
        $queryString = (empty($values)) ? '' : ('?' . http_build_query($values));
        return '/' . strtr($regex, $replace) . $ext . $queryString;
    }
    
    /**
     * 获取路由规则的需求集
     *
     * @param string $regex
     *
     * @return array
     */
    private function _get_regex_require(string $regex): array
    {
        if (!isset($this->rules_require[$regex])) {
            preg_match_all('/:([A-z\d]+)/i', $regex, $matches);
            // 求交集
            $this->rules_require[$regex] = array_intersect($matches[0], array_keys($this->entities));
        }
        return $this->rules_require[$regex];
    }
}